<!DOCTYPE html>


<html lang="zh-CN">


<head>
  <meta charset="utf-8" />
    
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
  <title>
    全面解析Kafka 核心要素及其常见部署.md |  
  </title>
  <meta name="generator" content="hexo-theme-ayer">
  
  <link rel="shortcut icon" href="/favicon.ico" />
  
  
<link rel="stylesheet" href="/dist/main.css">

  
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Shen-Yu/cdn/css/remixicon.min.css">

  
<link rel="stylesheet" href="/css/custom.css">

  
  
<script src="https://cdn.jsdelivr.net/npm/pace-js@1.0.2/pace.min.js"></script>

  
  

  

</head>

</html>

<body>
  <div id="app">
    
      
    <main class="content on">
      <section class="outer">
  <article
  id="post-deploy/全面解析Kafka 核心要素及其常见部署"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h1 class="article-title sea-center" style="border-left:0" itemprop="name">
  全面解析Kafka 核心要素及其常见部署.md
</h1>
 

    </header>
     
    <div class="article-meta">
      <a href="/2020/11/11/deploy/%E5%85%A8%E9%9D%A2%E8%A7%A3%E6%9E%90Kafka%20%E6%A0%B8%E5%BF%83%E8%A6%81%E7%B4%A0%E5%8F%8A%E5%85%B6%E5%B8%B8%E8%A7%81%E9%83%A8%E7%BD%B2/" class="article-date">
  <time datetime="2020-11-10T16:00:00.000Z" itemprop="datePublished">2020-11-11</time>
</a> 
  <div class="article-category">
    <a class="article-category-link" href="/categories/deploy/">deploy</a>
  </div>
  
<div class="word_count">
    <span class="post-time">
        <span class="post-meta-item-icon">
            <i class="ri-quill-pen-line"></i>
            <span class="post-meta-item-text"> 字数统计:</span>
            <span class="post-count">3.9k</span>
        </span>
    </span>

    <span class="post-time">
        &nbsp; | &nbsp;
        <span class="post-meta-item-icon">
            <i class="ri-book-open-line"></i>
            <span class="post-meta-item-text"> 阅读时长≈</span>
            <span class="post-count">14 分钟</span>
        </span>
    </span>
</div>
 
    </div>
      
    <div class="tocbot"></div>




  
    <div class="article-entry" itemprop="articleBody">
       
  <h1 id="全面解析Kafka-核心要素及其常见部署"><a href="#全面解析Kafka-核心要素及其常见部署" class="headerlink" title="全面解析Kafka 核心要素及其常见部署"></a>全面解析Kafka 核心要素及其常见部署</h1><p>放眼当下数据为王的时代，深入了解 Apache Kafka 及其常见的部署应用，快速实现数据架构（Kafka Fast Data Architecture）已是大势所趋，刻不容缓。</p>
<p>以下分别 Kafka 架构，四大核心 API，典型应用场景，Kafka 代理与消息主题，集群的创建，流 APIs（Stream APIs）及其处理模式等不同方面展开详细介绍。</p>
<p>Kafka：分布式流平台</p>
<p>Kafka 是一个分布式流平台，用于发布和订阅消息流（也称记录流或数据流），快速有效地利用 I/O 进行数据流的批处理，压缩及解耦，并将数据流传输到数据池，应用程序和实时流分析系统中。</p>
<p>Kafka 将主题消息分区复制到多个服务器中，允许用户通过自己的应用程序来处理这些记录。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451249.webp" alt="img"></p>
<p>Kafka 四大核心 APIs</p>
<p>Kafka 由记录（records），主题（topics），使用者（consumers），生产者（producers），代理服务（brokers），日志（logs），分区（partitions）和集群（clusters）组成。</p>
<p>Kafka 主题是一个记录流，每个主题都有对应的日志，该日志是该主题在磁盘上的存储，每个主题日志又分为多个分区和片段。</p>
<p>Kafka Producer API 用于生成数据记录流。Kafka Consumer API 用于消费来自 Kafka 的记录流。</p>
<p>Broker 是在 Kafka 集群中运行的 Kafka 服务器，Kafka 集群由多个代理服务器组成。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451247.webp" alt="img"></p>
<p><strong>①生产者 API（Producer API）：</strong>消息的生产者，向 Kafka broker 发消息的客户端。</p>
<p>允许客户端与集群中运行着的 Kafka 服务器相连接，并将记录流发布到一个或多个 Kafka topics（消息主题）中。</p>
<p>一台 Kafka 服务器就是一个 broker，一个集群由多个 broker 组成，一个 broker 可以容纳多个 topic。</p>
<p><strong>②消费者 API（Consumer API）：</strong>消息消费者，向 Kafka broker 获取消息的客户端。</p>
<p>允许客户端连接集群中运行着的 Kafka 服务器，并消费其中一个或多个 Kafka topics（消息主题）的记录流。</p>
<p><strong>③流 API（Stream API）：</strong>充当流处理器，用于输入输出流的转换。</p>
<p>允许客户端充当流处理器，从一个或多个 topics（消息主题）消费输入流，并生产输出流，输出到一个或多个其他 topics（消息主题）中，从而有效地将输入流转换至输出流。</p>
<p><strong>④连接器 API（Connector API）：</strong>允许编写可重用的生产者和消费者代码。</p>
<p>我们可以从任何关系型数据库中读取数据，并将其发布到主题中，同时也可以“消费”这个主题中的数据，并将其写入关系型数据库。</p>
<p>由此可见，Connector API 支持构建和运行可重复使用的生产者或消费者，并将 topic 连接到现有的应用程序或数据系统。（例如，就关系型数据库而言，其连接器可以捕获到各个表中的每个变化。）</p>
<p>Kafka应用场景</p>
<p><strong>消息系统</strong></p>
<p>Kafka 作为企业消息传递系统，通过源系统及目标系统间的分离来实现数据交换。与 JMS 相比，Kafka 兼具高吞吐量分区及高可靠容错力的复制功能。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451096.webp" alt="img"></p>
<p><strong>Web 站点活动跟踪</strong></p>
<p>跟踪记录用户在网站上的所有事件信息，从而进行数据的分析及脱机处理。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451250.webp" alt="img"></p>
<p><strong>日志汇总</strong></p>
<p>用于处理来自不同系统的日志，尤其是那些处于微服务架构分布式环境中的系统，这类系统通常部署在不同的主机上，因此 Kafka 需要汇总来自不同系统的各类日志，进而对这些日志集中进行分析处理。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451252.webp" alt="img"></p>
<p><strong>指标收集</strong></p>
<p>Kafka 可用于收集来自各类系统/网络的指标，并进行监控，Kafka 配有专门的指标报告生成工具，如 Ganglia，Graphite 等。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451243.webp" alt="img"></p>
<p>Kafka Brokers &amp; Kafka Topics</p>
<p><strong>Kafka Broker（代理服务器）</strong></p>
<p>Kafka 集群中的一个实例称之为代理（服务器），在 Kafka 集群中，只要连接其中任意一个代理（服务器）就能访问到整个集群，每个代理在集群中通过 ID 进行标识。</p>
<p><strong>Kafka Topics（消息主题）</strong></p>
<p>一个消息主题（Topic）是一个消息记录发布后的逻辑名称，在 Kafka 中，Topic 被分为若干个分区（Partitions），用于消息的发布。</p>
<p>这些分区分布在集群的各个代理服务器（Brokers）中，为了实现可扩展性，通常将一个非常大的 Topic 分布在多个代理服务器（Broker）上。</p>
<p>由于一个 Topic 可以分为多个分区（Partition），每个分区（Partition）都是一个有序的队列。</p>
<p>分区（Partition）中的每条消息都会被分配一个有序的 ID（即偏移量，Offset）。</p>
<p>如下图所示，假设当前有一个主题（Topic），该主题（Topic）有三个分区，集群中有三个代理（Broker），则每个代理都有一个分区。要发布到分区的数据以偏移量（Offset）增量的方式追加。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451253.webp" alt="img"></p>
<p>其中“Offset”即偏移量，Kafka 的存储文件都是按照“offset.kafka”来命名，用 Offset 方式命名是为了便于查找，如果想找位于 2046 的位置，只需找到 2045.kafka 的文件即可。</p>
<p>以下是分区（Partitions）使用时值得注意的要点：</p>
<ul>
<li>每个消息主题（Topic）按名称标识，集群中允许有多个已命名的消息主题。</li>
<li>每个消息前后顺序的有效性仅限于当前分区级别（maintained at the partition level），而非跨主题。</li>
<li>数据一旦写入分区，则不会被覆盖，这就是Kafka中强调的数据不变性（immutability）</li>
<li>分区中的消息通过键（key），值（values），时间戳（timestamps）的形式一起存储，Kafka 确保每一个给定密钥的消息都会发布到同一个分区中。</li>
<li>在 Kafka 集群中，每一个分区都有一个引导程序（leader），该引导程序负责对该分区执行读/写操作。</li>
</ul>
<p><img src="http://iubest.gitee.io/pic/640-1601003451248.webp" alt="img"></p>
<p>上图是一个例子，当前集群中仅一个消息主题（Topic），该主题包含三个分区（partition0，partition1，partition2），集群中有三个代理服务器（broker1，broker2，broker3）。</p>
<p>当前每个分区的副本都复制到另外两个代理服务器（Broker）中，即每个代理服务器（Broker）上包含了三个分区。</p>
<p>因此即便其中某两个代理服务器（Broker）发生故障，也不用担心数据会丢失。</p>
<p>如上，当我们在 Kafka 中创建主题时，始终建议确保主题（Topic）的复制因子大于 1，并且小于/等于集群中的代理服务器（Broker）数量，这是非常推荐的做法。</p>
<p>上图示例中，当前主题的复制因子为 3（即，一份原始数据，两份副本数据）， 不难推算出每个分区的引导程序加上其副本数量总共为“3”。</p>
<p>该示例中，每个分区都有一个引导程序（称之为“leader”），以及其他两个同步副本（称之为“follower”）。</p>
<p>对于分区 partition 0 来说，broker1 是“leader”， broker2 和 broker3 都是“follower”，从而分区 partition 0 的所有读写操作都将在 broker1 中进行。</p>
<p>同时，之后更新的内容也会被同步复制到 broker2 和 broker3 对应的分区（partition）中。</p>
<p>创建 Kafka 集群——Demo</p>
<p>我们还是以上图中三个 Broker 组成的 Kafka 集群为例，拆解 Kafka 集群创建的步骤。</p>
<p><strong>①Kafka 集群环境准备</strong></p>
<p>首先需要准备好一台安装有 Zookeeper 的机器，没有 Zookeeper，Kafka 集群将无法工作。</p>
<p>同时建议直接从官网下载最新版本的 Apache Kafka，目前版本更新至2.11，直接解压后将其放置到 bin 目录下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https:&#x2F;&#x2F;archive.apache.org&#x2F;dist&#x2F;kafka&#x2F;1.0.0&#x2F;kafka_2.11-1.0.0.tgz</span><br></pre></td></tr></table></figure>



<p>然后启动 ZooKeeper，为什么需要 Zookeeper？它在这里主要负责协调服务，管理代理服务 Broker，确定每个分区中的引导程序，以及在 Kafka 消息主题或代理服务发生变更时及时发出警告。</p>
<p>通过以下命令可以启动一个Zookeeper实例：</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451242.webp" alt="img"></p>
<p><strong>②启动 Kafka Brokers</strong></p>
<p>成功安装 Kafka 并启动 ZooKeeper 实例后，接下来就可以开启 Kafka Broker 了，这里共启动了三个 Kafka Broker。</p>
<p>具体启动方式：先定位到 Kafka 根目录下的“config”文件夹下，找到“server.properties”文件，将其复制三次。</p>
<p>然后分别命名为server_1.properties，server_2.properties 以及 server_3.properties，并针对三个文件内容做如下编辑，直接保存即可：</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451186.webp" alt="img"></p>
<p>保存后通过命令开启这三个代理服务：</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451252.webp" alt="img"></p>
<p><strong>③创建主题</strong></p>
<p>通过如下命令创建消息主题：</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451128.webp" alt="img"></p>
<p><strong>④生成引导服务</strong></p>
<p>通过 Kafka 控制台生成器（Kafka console）指定任意一个代理服务地址，并基于之前创建的主题发布一些消息。</p>
<p>这个指定的代理服务就被视作为引导服务程序，用于访问整个集群。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451250.webp" alt="img"></p>
<p><strong>⑤“消费”消息</strong></p>
<p>通过 Kafka 控制台来使用消息，用户（即：消息消费者）需要指定任意一个代理服务（Broker）地址作为引导服务器。</p>
<p>在阅读消息时，用户（即：消息消费者）是看不到消息顺序的，上文中也提到过消息的先后顺序仅在分区级别（partition level）进行维护，而非主题级别（topic level）。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451251.webp" alt="img"></p>
<p>通过以下命令可以描述主题并查看各分区的分布情况，以及每个分区的引导服务器：</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451251.webp" alt="img"></p>
<p>从上面的执行结果可以看出：</p>
<ul>
<li>broker-1 是分区 0 的引导服务器。</li>
<li>broker-2 是分区 1 的引导服务器。</li>
<li>broker-3 是分区 2 的引导服务器。</li>
<li>broker-1，broker-2，broker-3 分别具有每个分区的副本（同步且相互备份）。</li>
</ul>
<p>Kafka Streams API</p>
<p>Kafka 常被用作将流数据实时传输到其他系统中，此时 Kafka 作为中间层，主要用来解耦分离实时数据管道。</p>
<p>Kafka 流是 Kafka 生态系统的一部分，它提供了实时分析的功能，支持将流数据传输到大数据平台或 RDBMS，Cassandra，Spark 中，以进行将来的数据分析。</p>
<p>Kafka Stream API 简单易用，通过其强大的技术能力可处理所有存储于其中的数据，同时该 API 也为我们提供了一套 Kafka 标准类的实现规则。</p>
<p>在实际工作中为了能够创建支持核心业务的实时应用程序，我们需要 Kafka Stream API 的大力协助。</p>
<p>Kafka Stream API 独特之处在于，通过其构建的应用程序都是普通应用程序。</p>
<p>所以这些应用程序可以像其他任何应用程序一样，进行打包，部署和监控，而无需单独安装专门的处理集群或类似基础架构，这些额外部署的基础架构往往比较耗钱。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451242.webp" alt="img"></p>
<p>流（Stream）是 Kafka Streams 提供的最重要的抽象对象，代表了无限且持续更新的数据集。</p>
<p>流是一系列不可变数据记录的序列，具备有序，可重复，容错等特性，我们可以简单将其视为记录流（定义为：KStream）或变更日志流（定位为：KTable 或 GlobelKTable）。</p>
<p>流处理器（Stream Processor）是处理器拓扑结构中的一个节点，包含应用于流数据的处理逻辑，一系列节点组成了拓扑结构中的处理步骤（用于转换数据）。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451135.webp" alt="img"></p>
<p>Kafka Streams API 处理数据——Demo</p>
<p>Kafka Stream API 为实现流数据处理，即消息在 Kafka 中的消费及回写，提供了两种选项：</p>
<ul>
<li>高级 Kafka Streams DSL（high-level DSL）。</li>
<li>低级处理器 API：用于数据基本处理，组合处理，本地状态存储。</li>
</ul>
<p><strong>①高级 DSL（high-level DSL）</strong></p>
<p>高级 DSL 由记录流（KStream） 和日志流（KTable/GlobalKTable）两大主要抽象类别组成，包含一系列已实现的方法可供调用。</p>
<p>KStream 是记录流的抽象，其中每个数据都是无限数据集中的简单键值，KStream 提供了多种处理数据流的功能。</p>
<p>例如：map，mapValue，flatMap，flatMapValues，filter；同时还支持多个流连接，流数据的聚合。</p>
<p>KTable 是变更日志流的抽象，在变更日志中，对具有相同键的行（row）进行覆盖，因而每条数据记录都被视作为插入或更新。</p>
<p><strong>②处理器 API（lower-level processor ）</strong></p>
<p>低级处理器 API 通过扩展抽象类（AbstractProcessor），覆盖含有业务逻辑的处理方法，从而实现客户端流数据的访问，允许基于输入数据流执行相应的业务逻辑，同时将其结果作为下游数据转发至客户端。</p>
<p>相较于高级 DSL 提供具有功能样式的即用型方法，低级处理器API则按需提供处理逻辑。</p>
<p><strong>③Kafka Stream API 应用——高级 DSL Demo</strong></p>
<p>前提：必须在当前环境中有以下依赖，版本视当前情况而定。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451139.webp" alt="img"></p>
<p>导入以下包：</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451133.webp" alt="img"></p>
<p>Kafka 配置属性：</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451252.webp" alt="img"></p>
<p>实例化 KStreamBuilder，创建一个 KStream 对象：</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451247.webp" alt="img"></p>
<p>KStreamBuilder 有个 Stream 方法，该方法以主题名称（topic name）作为参数，返回一个 KStream 对象，即，订阅了指定主题的实例化对象。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451249.webp" alt="img"></p>
<p>基于 KStream 对象，这时我们就可以使用 Kafka Streams 高级 DSL 提供的众多方法（例如：map，process，transform，join 等），然后将处理后的数据发送到另一个主题。</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451248.webp" alt="img"></p>
<p>最后，通过构建器（builder）和流配置进行流式传输：</p>
<p><img src="http://iubest.gitee.io/pic/640-1601003451248.webp" alt="img"></p>
<p>通过 Kafka Streams API，我们无需单独部署集群即可在 Kafka 中进行数据流处理。</p>
<p>Kafka Streams API 给我们带来的便捷主要包含以下几个方面：</p>
<ul>
<li>高可扩展性，灵活性，分布式和容错性。</li>
<li>支持有状态和无状态处理。</li>
<li>具有窗口，联接和聚合的事件时间处理。</li>
<li>通过 Kafka Streams DSL 或较低级别的处理器 API 使用已经定义的常见转换操作。</li>
<li>对处理没有单独的群集要求（与 Kafka 集成）。</li>
<li>采用一次一个记录的处理以实现毫秒级的处理延迟。</li>
<li>支持 Kafka Connect 连接到不同的应用程序和数据库。</li>
</ul>
<p>总结</p>
<p>Kafka 的便捷操作是其备受业内人士广泛关注的原因之一，然而更重要的是其出色的稳定性，可靠性及耐用性，且具有灵活的发布/队列，可以很好地适应 N 个消费者组，具有强大的可复制性，可以为生产者提供一致性保证。</p>
<p>本次分享基于 Kafka 核心要素及其常见部署做了详情解析，希望给圈内感兴趣的人士提供技术普及，交流互补。</p>
<p><em>作者：\</em>罗小罗**</p>
<p><strong>简介：英国 TOP10 计算机专业，计算机科学与技术硕士，先后就职于汇丰，JPMorgan，HP，交行，阿里等国内外知名企业。涉及项目领域主要有：互联网金融，电商，教育，医疗等。现任就职于某世界 500 强公司，担任测试开发团队负责人，带领团队构建并持续优化自动化测试框架，研发自动化测试辅助类工具；擅长领域：单元/接口/性能/安全/自动化测试/CD/CI/DevOps；个人持续研究领域：自动化测试模型/数据分析/算法/机器学习等。</strong></p>
 
      <!-- reward -->
      
      <div id="reword-out">
        <div id="reward-btn">
          打赏
        </div>
      </div>
      
    </div>
    

    <!-- copyright -->
    
    <div class="declare">
      <ul class="post-copyright">
        <li>
          <i class="ri-copyright-line"></i>
          <strong>版权声明： </strong>
          
          本博客所有文章除特别声明外，著作权归作者所有。转载请注明出处！
          
        </li>
      </ul>
    </div>
    
    <footer class="article-footer">
       
<div class="share-btn">
      <span class="share-sns share-outer">
        <i class="ri-share-forward-line"></i>
        分享
      </span>
      <div class="share-wrap">
        <i class="arrow"></i>
        <div class="share-icons">
          
          <a class="weibo share-sns" href="javascript:;" data-type="weibo">
            <i class="ri-weibo-fill"></i>
          </a>
          <a class="weixin share-sns wxFab" href="javascript:;" data-type="weixin">
            <i class="ri-wechat-fill"></i>
          </a>
          <a class="qq share-sns" href="javascript:;" data-type="qq">
            <i class="ri-qq-fill"></i>
          </a>
          <a class="douban share-sns" href="javascript:;" data-type="douban">
            <i class="ri-douban-line"></i>
          </a>
          <!-- <a class="qzone share-sns" href="javascript:;" data-type="qzone">
            <i class="icon icon-qzone"></i>
          </a> -->
          
          <a class="facebook share-sns" href="javascript:;" data-type="facebook">
            <i class="ri-facebook-circle-fill"></i>
          </a>
          <a class="twitter share-sns" href="javascript:;" data-type="twitter">
            <i class="ri-twitter-fill"></i>
          </a>
          <a class="google share-sns" href="javascript:;" data-type="google">
            <i class="ri-google-fill"></i>
          </a>
        </div>
      </div>
</div>

<div class="wx-share-modal">
    <a class="modal-close" href="javascript:;"><i class="ri-close-circle-line"></i></a>
    <p>扫一扫，分享到微信</p>
    <div class="wx-qrcode">
      <img src="//api.qrserver.com/v1/create-qr-code/?size=150x150&data=http://example.com/2020/11/11/deploy/%E5%85%A8%E9%9D%A2%E8%A7%A3%E6%9E%90Kafka%20%E6%A0%B8%E5%BF%83%E8%A6%81%E7%B4%A0%E5%8F%8A%E5%85%B6%E5%B8%B8%E8%A7%81%E9%83%A8%E7%BD%B2/" alt="微信分享二维码">
    </div>
</div>

<div id="share-mask"></div>  
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/deploy/" rel="tag">deploy</a></li></ul>

    </footer>
  </div>

   
  <nav class="article-nav">
    
      <a href="/2020/11/11/tips/%E7%94%A8%E5%8A%A8%E5%9B%BE%E5%B1%95%E7%A4%BA10%E5%A4%A7Git%E5%91%BD%E4%BB%A4/" class="article-nav-link">
        <strong class="article-nav-caption">上一篇</strong>
        <div class="article-nav-title">
          
            用动图展示10大Git命令.md
          
        </div>
      </a>
    
    
      <a href="/2020/11/11/deploy/%E9%AB%98%E5%8F%AF%E7%94%A8%E7%9A%84Redis%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E9%9B%86%E7%BE%A4%EF%BC%8C%E4%BB%8E%E7%90%86%E8%AE%BA%E5%88%B0%E5%AE%9E%E8%B7%B5/" class="article-nav-link">
        <strong class="article-nav-caption">下一篇</strong>
        <div class="article-nav-title">高可用的Redis主从复制集群，从理论到实践.md</div>
      </a>
    
  </nav>

   
<!-- valine评论 -->
<div id="vcomments-box">
  <div id="vcomments"></div>
</div>
<script src="//cdn1.lncld.net/static/js/3.0.4/av-min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/valine@1.4.14/dist/Valine.min.js"></script>
<script>
  new Valine({
    el: "#vcomments",
    app_id: "",
    app_key: "",
    path: window.location.pathname,
    avatar: "monsterid",
    placeholder: "给我的文章加点评论吧~",
    recordIP: true,
  });
  const infoEle = document.querySelector("#vcomments .info");
  if (infoEle && infoEle.childNodes && infoEle.childNodes.length > 0) {
    infoEle.childNodes.forEach(function (item) {
      item.parentNode.removeChild(item);
    });
  }
</script>
<style>
  #vcomments-box {
    padding: 5px 30px;
  }

  @media screen and (max-width: 800px) {
    #vcomments-box {
      padding: 5px 0px;
    }
  }

  #vcomments-box #vcomments {
    background-color: #fff;
  }

  .v .vlist .vcard .vh {
    padding-right: 20px;
  }

  .v .vlist .vcard {
    padding-left: 10px;
  }
</style>

 
     
</article>

</section>
      <footer class="footer">
  <div class="outer">
    <ul>
      <li>
        Copyrights &copy;
        2015-2020
        <i class="ri-heart-fill heart_icon"></i> TzWind
      </li>
    </ul>
    <ul>
      <li>
        
        
        
        由 <a href="https://hexo.io" target="_blank">Hexo</a> 强力驱动
        <span class="division">|</span>
        主题 - <a href="https://github.com/Shen-Yu/hexo-theme-ayer" target="_blank">Ayer</a>
        
      </li>
    </ul>
    <ul>
      <li>
        
        
        <span>
  <span><i class="ri-user-3-fill"></i>访问人数:<span id="busuanzi_value_site_uv"></span></s>
  <span class="division">|</span>
  <span><i class="ri-eye-fill"></i>浏览次数:<span id="busuanzi_value_page_pv"></span></span>
</span>
        
      </li>
    </ul>
    <ul>
      
    </ul>
    <ul>
      
    </ul>
    <ul>
      <li>
        <!-- cnzz统计 -->
        
        <script type="text/javascript" src='https://s9.cnzz.com/z_stat.php?id=1278069914&amp;web_id=1278069914'></script>
        
      </li>
    </ul>
  </div>
</footer>
      <div class="float_btns">
        <div class="totop" id="totop">
  <i class="ri-arrow-up-line"></i>
</div>

<div class="todark" id="todark">
  <i class="ri-moon-line"></i>
</div>

      </div>
    </main>
    <aside class="sidebar on">
      <button class="navbar-toggle"></button>
<nav class="navbar">
  
  <div class="logo">
    <a href="/"><img src="/images/ayer-side.svg" alt="Hexo"></a>
  </div>
  
  <ul class="nav nav-main">
    
    <li class="nav-item">
      <a class="nav-item-link" href="/">主页</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/archives">归档</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/categories">分类</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/tags">标签</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" target="_blank" rel="noopener" href="http://www.baidu.com">百度</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/friends">友链</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/2019/about">关于我</a>
    </li>
    
  </ul>
</nav>
<nav class="navbar navbar-bottom">
  <ul class="nav">
    <li class="nav-item">
      
      <a class="nav-item-link nav-item-search"  title="搜索">
        <i class="ri-search-line"></i>
      </a>
      
      
      <a class="nav-item-link" target="_blank" href="/atom.xml" title="RSS Feed">
        <i class="ri-rss-line"></i>
      </a>
      
    </li>
  </ul>
</nav>
<div class="search-form-wrap">
  <div class="local-search local-search-plugin">
  <input type="search" id="local-search-input" class="local-search-input" placeholder="Search...">
  <div id="local-search-result" class="local-search-result"></div>
</div>
</div>
    </aside>
    <script>
      if (window.matchMedia("(max-width: 768px)").matches) {
        document.querySelector('.content').classList.remove('on');
        document.querySelector('.sidebar').classList.remove('on');
      }
    </script>
    <div id="mask"></div>

<!-- #reward -->
<div id="reward">
  <span class="close"><i class="ri-close-line"></i></span>
  <p class="reward-p"><i class="ri-cup-line"></i>请我喝杯咖啡吧~</p>
  <div class="reward-box">
    
    
  </div>
</div>
    
<script src="/js/jquery-2.0.3.min.js"></script>


<script src="/js/lazyload.min.js"></script>

<!-- Tocbot -->


<script src="/js/tocbot.min.js"></script>

<script>
  tocbot.init({
    tocSelector: '.tocbot',
    contentSelector: '.article-entry',
    headingSelector: 'h1, h2, h3, h4, h5, h6',
    hasInnerContainers: true,
    scrollSmooth: true,
    scrollContainer: 'main',
    positionFixedSelector: '.tocbot',
    positionFixedClass: 'is-position-fixed',
    fixedSidebarOffset: 'auto'
  });
</script>

<script src="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.css">
<script src="https://cdn.jsdelivr.net/npm/justifiedGallery@3.7.0/dist/js/jquery.justifiedGallery.min.js"></script>

<script src="/dist/main.js"></script>

<!-- ImageViewer -->

<!-- Root element of PhotoSwipe. Must have class pswp. -->
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

    <!-- Background of PhotoSwipe. 
         It's a separate element as animating opacity is faster than rgba(). -->
    <div class="pswp__bg"></div>

    <!-- Slides wrapper with overflow:hidden. -->
    <div class="pswp__scroll-wrap">

        <!-- Container that holds slides. 
            PhotoSwipe keeps only 3 of them in the DOM to save memory.
            Don't modify these 3 pswp__item elements, data is added later on. -->
        <div class="pswp__container">
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
        </div>

        <!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
        <div class="pswp__ui pswp__ui--hidden">

            <div class="pswp__top-bar">

                <!--  Controls are self-explanatory. Order can be changed. -->

                <div class="pswp__counter"></div>

                <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>

                <button class="pswp__button pswp__button--share" style="display:none" title="Share"></button>

                <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>

                <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>

                <!-- Preloader demo http://codepen.io/dimsemenov/pen/yyBWoR -->
                <!-- element will get class pswp__preloader--active when preloader is running -->
                <div class="pswp__preloader">
                    <div class="pswp__preloader__icn">
                        <div class="pswp__preloader__cut">
                            <div class="pswp__preloader__donut"></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
                <div class="pswp__share-tooltip"></div>
            </div>

            <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
            </button>

            <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
            </button>

            <div class="pswp__caption">
                <div class="pswp__caption__center"></div>
            </div>

        </div>

    </div>

</div>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/default-skin/default-skin.min.css">
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe-ui-default.min.js"></script>

<script>
    function viewer_init() {
        let pswpElement = document.querySelectorAll('.pswp')[0];
        let $imgArr = document.querySelectorAll(('.article-entry img:not(.reward-img)'))

        $imgArr.forEach(($em, i) => {
            $em.onclick = () => {
                // slider展开状态
                // todo: 这样不好，后面改成状态
                if (document.querySelector('.left-col.show')) return
                let items = []
                $imgArr.forEach(($em2, i2) => {
                    let img = $em2.getAttribute('data-idx', i2)
                    let src = $em2.getAttribute('data-target') || $em2.getAttribute('src')
                    let title = $em2.getAttribute('alt')
                    // 获得原图尺寸
                    const image = new Image()
                    image.src = src
                    items.push({
                        src: src,
                        w: image.width || $em2.width,
                        h: image.height || $em2.height,
                        title: title
                    })
                })
                var gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, {
                    index: parseInt(i)
                });
                gallery.init()
            }
        })
    }
    viewer_init()
</script>

<!-- MathJax -->

<!-- Katex -->

<!-- busuanzi  -->


<script src="/js/busuanzi-2.3.pure.min.js"></script>


<!-- ClickLove -->

<!-- ClickBoom1 -->

<!-- ClickBoom2 -->

<!-- CodeCopy -->


<link rel="stylesheet" href="/css/clipboard.css">

<script src="https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js"></script>
<script>
  function wait(callback, seconds) {
    var timelag = null;
    timelag = window.setTimeout(callback, seconds);
  }
  !function (e, t, a) {
    var initCopyCode = function(){
      var copyHtml = '';
      copyHtml += '<button class="btn-copy" data-clipboard-snippet="">';
      copyHtml += '<i class="ri-file-copy-2-line"></i><span>COPY</span>';
      copyHtml += '</button>';
      $(".highlight .code pre").before(copyHtml);
      $(".article pre code").before(copyHtml);
      var clipboard = new ClipboardJS('.btn-copy', {
        target: function(trigger) {
          return trigger.nextElementSibling;
        }
      });
      clipboard.on('success', function(e) {
        let $btn = $(e.trigger);
        $btn.addClass('copied');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-checkbox-circle-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPIED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-checkbox-circle-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
      clipboard.on('error', function(e) {
        e.clearSelection();
        let $btn = $(e.trigger);
        $btn.addClass('copy-failed');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-time-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPY FAILED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-time-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
    }
    initCopyCode();
  }(window, document);
</script>


<!-- CanvasBackground -->


    
  </div>
</body>

</html>